/**
 * Copyright 2012 Facebook
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.*;
import android.content.pm.*;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.*;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.CookieSyncManager;
import com.facebook.android.DialogError;
import com.facebook.android.Facebook.DialogListener;
import com.facebook.android.FacebookError;
import com.facebook.android.FbDialog;
import com.facebook.android.Util;

import java.io.*;
import java.lang.ref.WeakReference;
import java.util.*;

/**
 * <p>
 * Session is used to authenticate a user and manage the user's session with
 * Facebook.
 * </p>
 * <p>
 * Sessions must be opened before they can be used to make a Request. When a
 * Session is created, it attempts to initialize itself from a TokenCache.
 * Closing the session can optionally clear this cache.  The Session lifecycle
 * uses {@link SessionState SessionState} to indicate its state.
 * </p>
 * <p>
 * Instances of Session provide state change notification via a callback
 * interface, {@link Session.StatusCallback StatusCallback}.
 * </p>
 */
public class Session implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * The logging tag used by Session.
     */
    public static final String TAG = Session.class.getCanonicalName();

    /**
     * The default activity code used for authorization.
     * 
     * @see #openForRead(OpenRequest)
     *      open
     */
    public static final int DEFAULT_AUTHORIZE_ACTIVITY_CODE = 0xface;

    /**
     * If Session authorization fails and provides a web view error code, the
     * web view error code is stored in the Bundle returned from
     * {@link #getAuthorizationBundle getAuthorizationBundle} under this key.
     */
    public static final String WEB_VIEW_ERROR_CODE_KEY = "com.facebook.sdk.WebViewErrorCode";

    /**
     * If Session authorization fails and provides a failing url, the failing
     * url is stored in the Bundle returned from {@link #getAuthorizationBundle
     * getAuthorizationBundle} under this key.
     */
    public static final String WEB_VIEW_FAILING_URL_KEY = "com.facebook.sdk.FailingUrl";

    /**
     * The action used to indicate that the active session has been set. This should
     * be used as an action in an IntentFilter and BroadcastReceiver registered with
     * the {@link android.support.v4.content.LocalBroadcastManager}.
     */
    public static final String ACTION_ACTIVE_SESSION_SET = "com.facebook.sdk.ACTIVE_SESSION_SET";

    /**
     * The action used to indicate that the active session has been set to null. This should
     * be used as an action in an IntentFilter and BroadcastReceiver registered with
     * the {@link android.support.v4.content.LocalBroadcastManager}.
     */
    public static final String ACTION_ACTIVE_SESSION_UNSET = "com.facebook.sdk.ACTIVE_SESSION_UNSET";

    /**
     * The action used to indicate that the active session has been opened. This should
     * be used as an action in an IntentFilter and BroadcastReceiver registered with
     * the {@link android.support.v4.content.LocalBroadcastManager}.
     */
    public static final String ACTION_ACTIVE_SESSION_OPENED = "com.facebook.sdk.ACTIVE_SESSION_OPENED";

    /**
     * The action used to indicate that the active session has been closed. This should
     * be used as an action in an IntentFilter and BroadcastReceiver registered with
     * the {@link android.support.v4.content.LocalBroadcastManager}.
     */
    public static final String ACTION_ACTIVE_SESSION_CLOSED = "com.facebook.sdk.ACTIVE_SESSION_CLOSED";
    
    /**
     * Session takes application id as a constructor parameter. If this is null,
     * Session will attempt to load the application id from
     * application/meta-data using this String as the key.
     */
    public static final String APPLICATION_ID_PROPERTY = "com.facebook.sdk.ApplicationId";

    private static Object staticLock = new Object();
    private static Session activeSession;
    private static volatile Context staticContext;

    // Token extension constants
    private static final int TOKEN_EXTEND_THRESHOLD_SECONDS = 24 * 60 * 60; // 1
                                                                            // day
    private static final int TOKEN_EXTEND_RETRY_SECONDS = 60 * 60; // 1 hour

    private static final String SESSION_BUNDLE_SAVE_KEY = "com.facebook.sdk.Session.saveSessionKey";
    private static final String AUTH_BUNDLE_SAVE_KEY = "com.facebook.sdk.Session.authBundleKey";
    private static final String PUBLISH_PERMISSION_PREFIX = "publish";
    private static final String MANAGE_PERMISSION_PREFIX = "manage";
    @SuppressWarnings("serial")
    private static final Set<String> OTHER_PUBLISH_PERMISSIONS = new HashSet<String>() {{
        add("ads_management");
        add("create_event");
        add("rsvp_event");
    }};

    private String applicationId;
    private SessionState state;
    private AccessToken tokenInfo;
    private Date lastAttemptedTokenExtendDate = new Date(0);
    private boolean shouldAutoPublish = true;

    private AuthorizationRequest pendingRequest;

    // The following are not serialized with the Session object
    private volatile Bundle authorizationBundle;
    private List<StatusCallback> callbacks;
    private Handler handler;
    private AutoPublishAsyncTask autoPublishAsyncTask;
    // This is the object that synchronizes access to state and tokenInfo
    private Object lock = new Object();
    private TokenCache tokenCache;
    private volatile TokenRefreshRequest currentTokenRefreshRequest;

    /**
     * Serialization proxy for the Session class. This is version 1 of
     * serialization. Future serializations may differ in format. This
     * class should not be modified. If serializations formats change,
     * create a new class SerializationProxyVx.
     */
    private static class SerializationProxyV1 implements Serializable {
        private static final long serialVersionUID = 7663436173185080063L;
        private final String applicationId;
        private final SessionState state;
        private final AccessToken tokenInfo;
        private final Date lastAttemptedTokenExtendDate;
        private final boolean shouldAutoPublish;
        private final AuthorizationRequest pendingRequest;

        SerializationProxyV1(String applicationId, SessionState state,
                AccessToken tokenInfo, Date lastAttemptedTokenExtendDate,
                boolean shouldAutoPublish, AuthorizationRequest pendingRequest) {
            this.applicationId = applicationId;
            this.state = state;
            this.tokenInfo = tokenInfo;
            this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate;
            this.shouldAutoPublish = shouldAutoPublish;
            this.pendingRequest = pendingRequest;
        }

        private Object readResolve() {
            return new Session(applicationId, state, tokenInfo,
                    lastAttemptedTokenExtendDate, shouldAutoPublish, pendingRequest);
        }
    }

    /**
     * Used by version 1 of the serialization proxy, do not modify.
     */
    private Session(String applicationId, SessionState state,
            AccessToken tokenInfo, Date lastAttemptedTokenExtendDate,
            boolean shouldAutoPublish, AuthorizationRequest pendingRequest) {
        this.applicationId = applicationId;
        this.state = state;
        this.tokenInfo = tokenInfo;
        this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate;
        this.shouldAutoPublish = shouldAutoPublish;
        this.pendingRequest = pendingRequest;
        lock = new Object();
        handler = new Handler(Looper.getMainLooper());
        currentTokenRefreshRequest = null;
        tokenCache = null;
        callbacks = new ArrayList<StatusCallback>();
    }

    /**
     * Initializes a new Session with the specified context.
     * 
     * @param currentContext
     *            The Activity or Service creating this Session.
     */
    public Session(Context currentContext) {
        this(currentContext, null, null, true);
    }

    Session(Context context, String applicationId, TokenCache tokenCache, boolean shouldAutoPublish) {
        // if the application ID passed in is null, try to get it from the
        // meta-data in the manifest.
        if ((context != null) && (applicationId == null)) {
            applicationId = getMetadataApplicationId(context);
        }

        Validate.notNull(applicationId, "applicationId");

        initializeStaticContext(context);

        if (tokenCache == null) {
            tokenCache = new SharedPreferencesTokenCache(staticContext);
        }

        this.applicationId = applicationId;
        this.tokenCache = tokenCache;
        this.state = SessionState.CREATED;
        this.pendingRequest = null;
        this.callbacks = new ArrayList<StatusCallback>();
        this.handler = new Handler(Looper.getMainLooper());
        this.shouldAutoPublish = shouldAutoPublish;

        Bundle tokenState = tokenCache.load();
        if (TokenCache.hasTokenInformation(tokenState)) {
            Date cachedExpirationDate = TokenCache.getDate(tokenState, TokenCache.EXPIRATION_DATE_KEY);
            Date now = new Date();

            if ((cachedExpirationDate == null) || cachedExpirationDate.before(now)) {
                // If expired or we require new permissions, clear out the
                // current token cache.
                tokenCache.clear();
                this.tokenInfo = AccessToken.createEmptyToken(Collections.<String>emptyList());
            } else {
                // Otherwise we have a valid token, so use it.
                this.tokenInfo = AccessToken.createFromCache(tokenState);
                this.state = SessionState.CREATED_TOKEN_LOADED;
            }
        } else {
            this.tokenInfo = AccessToken.createEmptyToken(Collections.<String>emptyList());
        }
    }

    /**
     * Returns a Bundle containing data that was returned from facebook during
     * authorization.
     * 
     * @return a Bundle containing data that was returned from facebook during
     *         authorization.
     */
    public final Bundle getAuthorizationBundle() {
        synchronized (this.lock) {
            return this.authorizationBundle;
        }
    }

    /**
     * Returns a boolean indicating whether the session is opened.
     * 
     * @return a boolean indicating whether the session is opened.
     */
    public final boolean isOpened() {
        synchronized (this.lock) {
            return this.state.isOpened();
        }
    }

    public final boolean isClosed() {
        synchronized (this.lock) {
            return this.state.isClosed();
        }
    }

    /**
     * Returns the current state of the Session.
     * See {@link SessionState} for details.
     * 
     * @return the current state of the Session.
     */
    public final SessionState getState() {
        synchronized (this.lock) {
            return this.state;
        }
    }

    /**
     * Returns the application id associated with this Session.
     * 
     * @return the application id associated with this Session.
     */
    public final String getApplicationId() {
        return this.applicationId;
    }

    /**
     * Returns the access token String.
     * 
     * @return the access token String.
     */
    public final String getAccessToken() {
        synchronized (this.lock) {
            return this.tokenInfo.getToken();
        }
    }

    /**
     * <p>
     * Returns the Date at which the current token will expire.
     * </p>
     * <p>
     * Note that Session automatically attempts to extend the lifetime of Tokens
     * as needed when facebook requests are made.
     * </p>
     * 
     * @return the Date at which the current token will expire.
     */
    public final Date getExpirationDate() {
        synchronized (this.lock) {
            return this.tokenInfo.getExpires();
        }
    }

    /**
     * <p>
     * Returns the list of permissions associated with the session.
     * </p>
     * <p>
     * If there is a valid token, this represents the permissions granted by
     * that token. This can change during calls to
     * {@link #reauthorizeForRead(com.facebook.Session.ReauthorizeRequest)}
     * or {@link #reauthorizeForPublish(com.facebook.Session.ReauthorizeRequest)}.
     * </p>
     * 
     * @return the list of permissions associated with the session.
     */
    public final List<String> getPermissions() {
        synchronized (this.lock) {
            return this.tokenInfo.getPermissions();
        }
    }

    /**
     * <p>
     * Logs a user in to Facebook.
     * </p>
     * <p>
     * A session may not be used with {@link Request Request} and other classes
     * in the SDK until it is open. If, prior to calling open, the session is in
     * the {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED}
     * state, and the requested permissions are a subset of the previously authorized
     * permissions, then the Session becomes usable immediately with no user interaction.
     * </p>
     * <p>
     * The permissions associated with the openRequest passed to this method must
     * be read permissions only (or null/empty). It is not allowed to pass publish
     * permissions to this method and will result in an exception being thrown.
     * </p>
     * <p>
     * Any open method must be called at most once, and cannot be called after the
     * Session is closed. Calling the method at an invalid time will result in
     * UnsuportedOperationException.
     * </p>
     *
     * @param openRequest
     *         the open request, can be null only if the Session is in the
     *         {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} state
     *
     * @throws FacebookException
     *         if any publish permissions are requested
     */
    public final void openForRead(OpenRequest openRequest) {
        open(openRequest, AuthorizationType.READ);
    }

    /**
     * <p>
     * Logs a user in to Facebook.
     * </p>
     * <p>
     * A session may not be used with {@link Request Request} and other classes
     * in the SDK until it is open. If, prior to calling open, the session is in
     * the {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED}
     * state, and the requested permissions are a subset of the previously authorized
     * permissions, then the Session becomes usable immediately with no user interaction.
     * </p>
     * <p>
     * The permissions associated with the openRequest passed to this method must
     * be publish permissions only and must be non-empty. Any read permissions
     * will result in a warning, and may fail during server-side authorization.
     * </p>
     * <p>
     * Any open method must be called at most once, and cannot be called after the
     * Session is closed. Calling the method at an invalid time will result in
     * UnsuportedOperationException.
     * </p>
     *
     * @param openRequest
     *         the open request, can be null only if the Session is in the
     *         {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} state
     *
     * @throws FacebookException
     *         if the passed in request is null or has no permissions set.
     */
    public final void openForPublish(OpenRequest openRequest) {
        open(openRequest, AuthorizationType.PUBLISH);
    }

    /**
     * <p>
     * Logs a user in to Facebook.
     * </p>
     * <p>
     * A session may not be used with {@link Request Request} and other classes
     * in the SDK until it is open. If, prior to calling open, the session is in
     * the {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED}
     * state, then the Session becomes usable immediately with no user interaction.
     * Otherwise, this will open the Session with basic permissions.
     * </p>
     * <p>
     * Any open method must be called at most once, and cannot be called after the
     * Session is closed. Calling the method at an invalid time will result in
     * UnsuportedOperationException.
     * </p>
     *
     * @param activity
     *         the Activity used to open the Session
     */
    public final void openForRead(Activity activity) {
        openForRead(new OpenRequest(activity));
    }

    /**
     * <p>
     * Logs a user in to Facebook.
     * </p>
     * <p>
     * A session may not be used with {@link Request Request} and other classes
     * in the SDK until it is open. If, prior to calling open, the session is in
     * the {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED}
     * state, then the Session becomes usable immediately with no user interaction.
     * Otherwise, this will open the Session with basic permissions.
     * </p>
     * <p>
     * Any open method must be called at most once, and cannot be called after the
     * Session is closed. Calling the method at an invalid time will result in
     * UnsuportedOperationException.
     * </p>
     *
     * @param fragment
     *         the Fragment used to open the Session
     */
    public final void openForRead(Fragment fragment) {
        openForRead(new OpenRequest(fragment));
    }

    /**
     * <p>
     * Logs a user in to Facebook.
     * </p>
     * <p>
     * This method should only be called if the session is in
     * the {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED}
     * state.
     * </p>
     * <p>
     * Any open method must be called at most once, and cannot be called after the
     * Session is closed. Calling the method at an invalid time will result in
     * UnsuportedOperationException.
     * </p>
     *
     * @throws UnsupportedOperationException
     *          If the session is in an invalid state.
     */
    public final void open() {
        if (state.equals(SessionState.CREATED_TOKEN_LOADED)) {
            openForRead((OpenRequest) null);
        } else {
            throw new UnsupportedOperationException(String.format(
                    "Cannot call open without an OpenRequest when the state is %s",
                    state.toString()));
        }
    }

    /**
     * <p>
     * Reauthorizes the Session, with additional read permissions.
     * </p>
     * <p>
     * If successful, this will update the set of permissions on this session to
     * match the newPermissions. If this fails, the Session remains unchanged.
     * </p>
     * <p>
     * The permissions associated with the reauthorizeRequest passed to this method must
     * be read permissions only (or null/empty). It is not allowed to pass publish
     * permissions to this method and will result in an exception being thrown.
     * </p>
     *
     * @param reauthorizeRequest the reauthorization request
     */
    public final void reauthorizeForRead(ReauthorizeRequest reauthorizeRequest) {
        reauthorize(reauthorizeRequest, AuthorizationType.READ);
    }

    /**
     * <p>
     * Reauthorizes the Session, with additional publish permissions.
     * </p>
     * <p>
     * If successful, this will update the set of permissions on this session to
     * match the newPermissions. If this fails, the Session remains unchanged.
     * </p>
     * <p>
     * The permissions associated with the reauthorizeRequest passed to this method must
     * be publish permissions only and must be non-empty. Any read permissions
     * will result in a warning, and may fail during server-side authorization.
     * </p>
     *
     * @param reauthorizeRequest the reauthorization request
     */
    public final void reauthorizeForPublish(ReauthorizeRequest reauthorizeRequest) {
        reauthorize(reauthorizeRequest, AuthorizationType.PUBLISH);
    }

    /**
     * Provides an implementation for {@link Activity#onActivityResult
     * onActivityResult} that updates the Session based on information returned
     * during the authorization flow. The Activity that calls open or
     * reauthorize should forward the resulting onActivityResult call here to
     * update the Session state based on the contents of the resultCode and
     * data.
     * 
     * @param currentActivity
     *            The Activity that is forwarding the onActivityResult call.
     * @param requestCode
     *            The requestCode parameter from the forwarded call. When this
     *            onActivityResult occurs as part of facebook authorization
     *            flow, this value is the activityCode passed to open or
     *            authorize.
     * @param resultCode
     *            An int containing the resultCode parameter from the forwarded
     *            call.
     * @param data
     *            The Intent passed as the data parameter from the forwarded
     *            call.
     * @return A boolean indicating whether the requestCode matched a pending
     *         authorization request for this Session.
     */
    public final boolean onActivityResult(Activity currentActivity, int requestCode, int resultCode, Intent data) {
        Validate.notNull(currentActivity, "currentActivity");

        initializeStaticContext(currentActivity);

        AuthorizationRequest currentRequest = null;
        AuthorizationRequest retryRequest = null;
        AccessToken newToken = null;
        Exception exception = null;

        synchronized (lock) {
            if (pendingRequest == null || (requestCode != pendingRequest.getRequestCode())) {
                return false;
            } else {
                currentRequest = pendingRequest;
            }
        }

        this.authorizationBundle = null;

        if (resultCode == Activity.RESULT_CANCELED) {
            if (data == null) {
                // User pressed the 'back' button
                exception = new FacebookOperationCanceledException("Log in was canceled by the user");
            } else {
                this.authorizationBundle = data.getExtras();
                exception = new FacebookAuthorizationException(this.authorizationBundle.getString("error"));
            }
        } else if (resultCode == Activity.RESULT_OK) {
            Validate.notNull(data, "data");

            this.authorizationBundle = data.getExtras();
            String error = this.authorizationBundle.getString("error");
            if (error == null) {
                error = this.authorizationBundle.getString("error_type");
            }
            if (error != null) {
                if (ServerProtocol.errorsProxyAuthDisabled.contains(error)) {
                    retryRequest = currentRequest.setLoginBehavior(SessionLoginBehavior.SUPPRESS_SSO);
                } else if (ServerProtocol.errorsUserCanceled.contains(error)) {
                    exception = new FacebookOperationCanceledException("User canceled log in.");
                } else {
                    String description = this.authorizationBundle.getString("error_description");
                    if (description != null) {
                        error = error + ": " + description;
                    }
                    exception = new FacebookAuthorizationException(error);
                }
            } else {
                newToken = AccessToken.createFromSSO(currentRequest.permissions, data);
            }
        }

        if (retryRequest != null) {
            synchronized (lock) {
                if (pendingRequest == currentRequest) {
                    pendingRequest = retryRequest;
                } else {
                    retryRequest = null;
                }
            }
            authorize(retryRequest);
        } else {
            finishAuth(newToken, exception);
        }

        return true;
    }

    /**
     * Closes the local in-memory Session object, but does not clear the
     * persisted token cache.
     */
    @SuppressWarnings("incomplete-switch")
    public final void close() {
        synchronized (this.lock) {
            final SessionState oldState = this.state;

            switch (this.state) {
            case CREATED:
            case OPENING:
                this.state = SessionState.CLOSED_LOGIN_FAILED;
                postStateChange(oldState, this.state, new FacebookException(
                        "Log in attempt aborted."));
                break;

            case CREATED_TOKEN_LOADED:
            case OPENED:
            case OPENED_TOKEN_UPDATED:
                this.state = SessionState.CLOSED;
                postStateChange(oldState, this.state, null);
                break;
            }
        }
    }

    /**
     * Closes the local in-memory Session object and clears any persisted token
     * cache related to the Session.
     */
    public final void closeAndClearTokenInformation() {
        if (this.tokenCache != null) {
            this.tokenCache.clear();
        }
        Utility.clearFacebookCookies(staticContext);
        close();
    }

    @Override
    public final String toString() {
        return new StringBuilder().append("{Session").append(" state:").append(this.state).append(", token:")
                .append((this.tokenInfo == null) ? "null" : this.tokenInfo).append(", appId:")
                .append((this.applicationId == null) ? "null" : this.applicationId).append("}").toString();
    }

    /**
     * <p>
     * Do not use this method.
     * </p>
     * <p>
     * Refreshes the token based on information obtained from the Facebook
     * class. This is exposed to enable the com.facebook.android.Facebook class
     * to refresh the token in its underlying Session. Normally Session
     * automatically updates its token. This is only provided for backwards
     * compatibility and may be removed in a future release.
     * </p>
     * 
     * @param bundle
     *            Opaque Bundle of data from the Facebook class.
     */
    public void internalRefreshToken(Bundle bundle) {
        synchronized (this.lock) {
            final SessionState oldState = this.state;

            switch (this.state) {
            case OPENED:
                this.state = SessionState.OPENED_TOKEN_UPDATED;
                postStateChange(oldState, this.state, null);
                break;
            case OPENED_TOKEN_UPDATED:
                break;
            default:
                // Silently ignore attempts to refresh token if we are not open
                Log.d(TAG, "refreshToken ignored in state " + this.state);
                return;
            }
            this.tokenInfo = AccessToken.createForRefresh(this.tokenInfo, bundle);
            if (this.tokenCache != null) {
                this.tokenCache.save(this.tokenInfo.toCacheBundle());
            }
        }
    }

    private Object writeReplace() {
        return new SerializationProxyV1(applicationId, state, tokenInfo,
                lastAttemptedTokenExtendDate, shouldAutoPublish, pendingRequest);
    }

    // have a readObject that throws to prevent spoofing
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("Cannot readObject, serialization proxy required");
    }

    /**
     * Save the Session object into the supplied Bundle.
     *
     * @param session the Session to save
     * @param bundle the Bundle to save the Session to
     */
    public static final void saveSession(Session session, Bundle bundle) {
        if (bundle != null && session != null) {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            try {
                new ObjectOutputStream(outputStream).writeObject(session);
            } catch (IOException e) {
                throw new FacebookException("Unable to save session.", e);
            }
            bundle.putByteArray(SESSION_BUNDLE_SAVE_KEY, outputStream.toByteArray());
            bundle.putBundle(AUTH_BUNDLE_SAVE_KEY, session.authorizationBundle);
        }
    }

    /**
     * Restores the saved session from a Bundle, if any. Returns the restored Session or
     * null if it could not be restored.
     *
     * @param context
     *            the Activity or Service creating the Session, must not be null
     * @param cache
     *            the TokenCache to use to load and store the token. If this is
     *            null, a default token cache that stores data in
     *            SharedPreferences will be used
     * @param callback
     *            the callback to notify for Session state changes, can be null
     * @param bundle
     *            the bundle to restore the Session from
     * @return the restored Session, or null
     */
    public static final Session restoreSession(
            Context context, TokenCache cache, StatusCallback callback, Bundle bundle) {
        if (bundle == null) {
            return null;
        }
        byte[] data = bundle.getByteArray(SESSION_BUNDLE_SAVE_KEY);
        if (data != null) {
            ByteArrayInputStream is = new ByteArrayInputStream(data);
            try {
                Session session = (Session) (new ObjectInputStream(is)).readObject();
                initializeStaticContext(context);
                if (cache != null) {
                    session.tokenCache = cache;
                } else {
                    session.tokenCache = new SharedPreferencesTokenCache(context);
                }
                if (callback != null) {
                    session.addCallback(callback);
                }
                session.authorizationBundle =  bundle.getBundle(AUTH_BUNDLE_SAVE_KEY);
                return session;
            } catch (ClassNotFoundException e) {
                Log.w(TAG, "Unable to restore session", e);
            } catch (IOException e) {
                Log.w(TAG, "Unable to restore session.", e);
            }
        }
        return null;
    }


    /**
     * Returns the current active Session, or null if there is none.
     * 
     * @return the current active Session, or null if there is none.
     */
    public static final Session getActiveSession() {
        synchronized (Session.staticLock) {
            return Session.activeSession;
        }
    }

    /**
     * <p>
     * Sets the current active Session.
     * </p>
     * <p>
     * The active Session is used implicitly by predefined Request factory
     * methods as well as optionally by UI controls in the sdk.
     * </p>
     * <p>
     * It is legal to set this to null, or to a Session that is not yet open.
     * </p>
     * 
     * @param session
     *            A Session to use as the active Session, or null to indicate
     *            that there is no active Session.
     */
    public static final void setActiveSession(Session session) {
        synchronized (Session.staticLock) {
            if (session != Session.activeSession) {
                Session oldSession = Session.activeSession;

                if (oldSession != null) {
                    oldSession.close();
                }

                Session.activeSession = session;

                if (oldSession != null) {
                    postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_UNSET);
                }

                if (session != null) {
                    postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_SET);

                    if (session.isOpened()) {
                        postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_OPENED);
                    }
                }
            }
        }
    }

    /**
     * Create a new Session, and if a token cache is available, open the
     * Session and make it active without any user interaction.
     *
     * @param context
     *         The Context creating this session
     * @return The new session or null if one could not be created
     */
    public static Session openActiveSession(Context context) {
        return openActiveSession(context, false, null);
    }

    /**
     * If allowLoginUI is true, this will create a new Session, make it active, and
     * open it. If the default token cache is not available, then this will request
     * basic permissions. If the default token cache is available and cached tokens
     * are loaded, this will use the cached token and associated permissions.
     * <p/>
     * If allowedLoginUI is false, this will only create the active session and open
     * it if it requires no user interaction (i.e. the token cache is available and
     * there are cached tokens).
     *
     * @param activity
     *            The Activity that is opening the new Session.
     * @param allowLoginUI
     *            if false, only sets the active session and opens it if it
     *            does not require user interaction
     * @return The new Session or null if one could not be created
     */
    public static Session openActiveSession(Activity activity, boolean allowLoginUI) {
        return openActiveSession(activity, allowLoginUI, (StatusCallback) null);
    }

    /**
     * If allowLoginUI is true, this will create a new Session, make it active, and
     * open it. If the default token cache is not available, then this will request
     * basic permissions. If the default token cache is available and cached tokens
     * are loaded, this will use the cached token and associated permissions.
     * <p/>
     * If allowedLoginUI is false, this will only create the active session and open
     * it if it requires no user interaction (i.e. the token cache is available and
     * there are cached tokens).
     * 
     * @param activity
     *            The Activity that is opening the new Session.
     * @param allowLoginUI
     *            if false, only sets the active session and opens it if it
     *            does not require user interaction
     * @param callback
     *            The {@link StatusCallback SessionStatusCallback} to
     *            notify regarding Session state changes.
     * @return The new Session or null if one could not be created
     */
    public static Session openActiveSession(Activity activity, boolean allowLoginUI,
            StatusCallback callback) {
        return openActiveSession(activity, allowLoginUI, new OpenRequest(activity).setCallback(callback));
    }

    /**
     * If allowLoginUI is true, this will create a new Session, make it active, and
     * open it. If the default token cache is not available, then this will request
     * basic permissions. If the default token cache is available and cached tokens
     * are loaded, this will use the cached token and associated permissions.
     * <p/>
     * If allowedLoginUI is false, this will only create the active session and open
     * it if it requires no user interaction (i.e. the token cache is available and
     * there are cached tokens).
     *
     * @param context
     *            The Activity or Service creating this Session
     * @param fragment
     *            The Fragment that is opening the new Session.
     * @param allowLoginUI
     *            if false, only sets the active session and opens it if it
     *            does not require user interaction
     * @return The new Session or null if one could not be created
     */
    public static Session openActiveSession(Context context, Fragment fragment, boolean allowLoginUI) {
        return openActiveSession(context, fragment, allowLoginUI, null);
    }

    /**
     * If allowLoginUI is true, this will create a new Session, make it active, and
     * open it. If the default token cache is not available, then this will request
     * basic permissions. If the default token cache is available and cached tokens
     * are loaded, this will use the cached token and associated permissions.
     * <p/>
     * If allowedLoginUI is false, this will only create the active session and open
     * it if it requires no user interaction (i.e. the token cache is available and
     * there are cached tokens).
     *
     * @param context
     *            The Activity or Service creating this Session
     * @param fragment
     *            The Fragment that is opening the new Session.
     * @param allowLoginUI
     *            if false, only sets the active session and opens it if it
     *            does not require user interaction
     * @param callback
     *            The {@link StatusCallback SessionStatusCallback} to
     *            notify regarding Session state changes.
     * @return The new Session or null if one could not be created
     */
    public static Session openActiveSession(Context context, Fragment fragment,
            boolean allowLoginUI, StatusCallback callback) {
        return openActiveSession(context, allowLoginUI, new OpenRequest(fragment).setCallback(callback));
    }

    private static Session openActiveSession(Context context, boolean allowLoginUI, OpenRequest openRequest) {
        Session session = new Builder(context).build();
        if (SessionState.CREATED_TOKEN_LOADED.equals(session.getState()) || allowLoginUI) {
            setActiveSession(session);
            if (openRequest != null) {
                session.openForRead(openRequest);
            } else {
                session.open();
            }
            return session;
        }
        return null;
    }

    static Context getStaticContext() {
        return staticContext;
    }

    static void initializeStaticContext(Context currentContext) {
        if ((currentContext != null) && (staticContext == null)) {
            Context applicationContext = currentContext.getApplicationContext();
            staticContext = (applicationContext != null) ? applicationContext : currentContext;
        }
    }

    void authorize(AuthorizationRequest request) {
        boolean started = false;

        autoPublishAsync();

        if (!started && request.allowKatana()) {
            started = tryKatanaProxyAuth(request);
        }
        if (!started && request.allowWebView()) {
            started = tryDialogAuth(request);
        }

        if (!started) {
            synchronized (this.lock) {
                final SessionState oldState = this.state;

                switch (this.state) {
                    case CLOSED:
                    case CLOSED_LOGIN_FAILED:
                        return;

                    default:
                        this.state = SessionState.CLOSED_LOGIN_FAILED;
                        postStateChange(oldState, this.state, new FacebookException("Log in attempt failed."));
                }
            }
        }
    }

    public final void addCallback(StatusCallback callback) {
        synchronized(callbacks) {
            if (callback != null && !callbacks.contains(callback)) {
                callbacks.add(callback);
            }
        }
    }

    public final void removeCallback(StatusCallback callback) {
        synchronized(callbacks) {
            callbacks.remove(callback);
        }
    }

    private void open(OpenRequest openRequest, AuthorizationType authType) {
        validatePermissions(openRequest, authType);
        validateLoginBehavior(openRequest);
        SessionState newState;
        synchronized (this.lock) {
            if (pendingRequest != null) {
                throw new UnsupportedOperationException(
                        "Session: an attempt was made to open a session that has a pending request.");
            }
            final SessionState oldState = this.state;

            switch (this.state) {
                case CREATED:
                    this.state = newState = SessionState.OPENING;
                    if (openRequest == null) {
                        throw new IllegalArgumentException("openRequest cannot be null when opening a new Session");
                    }
                    pendingRequest = openRequest;
                    break;
                case CREATED_TOKEN_LOADED:
                    if (openRequest != null && !Utility.isNullOrEmpty(openRequest.getPermissions())) {
                        if (!Utility.isSubset(openRequest.getPermissions(), getPermissions())) {
                            pendingRequest = openRequest;
                        }
                    }
                    if (pendingRequest == null) {
                        this.state = newState = SessionState.OPENED;
                    } else {
                        this.state = newState = SessionState.OPENING;
                    }
                    break;
                default:
                    throw new UnsupportedOperationException(
                            "Session: an attempt was made to open an already opened session.");
            }
            if (openRequest != null) {
                addCallback(openRequest.getCallback());
            }
            this.postStateChange(oldState, newState, null);
        }

        if (newState == SessionState.OPENING) {
            authorize(openRequest);
        }
    }

    private void reauthorize(ReauthorizeRequest reauthorizeRequest, AuthorizationType authType) {
        validatePermissions(reauthorizeRequest, authType);
        validateLoginBehavior(reauthorizeRequest);
        if (reauthorizeRequest != null) {
            synchronized (this.lock) {
                if (pendingRequest != null) {
                    throw new UnsupportedOperationException(
                            "Session: an attempt was made to reauthorize a session that has a pending request.");
                }
                switch (this.state) {
                    case OPENED:
                    case OPENED_TOKEN_UPDATED:
                        pendingRequest = reauthorizeRequest;
                        break;
                    default:
                        throw new UnsupportedOperationException(
                                "Session: an attempt was made to reauthorize a session that is not currently open.");
                }
            }

            authorize(reauthorizeRequest);
        }
    }

    private void validateLoginBehavior(AuthorizationRequest request) {
        if (request != null && !request.suppressLoginActivityVerification &&
                (SessionLoginBehavior.SSO_WITH_FALLBACK.equals(request.getLoginBehavior()) ||
                 SessionLoginBehavior.SUPPRESS_SSO.equals(request.getLoginBehavior()))) {
            Intent intent = new Intent();
            intent.setClass(getStaticContext(), LoginActivity.class);
            if (!resolveIntent(intent, false)) {
                throw new FacebookException(String.format(
                        "Cannot use SessionLoginBehavior %s when %s is not declared as an activity in AndroidManifest.xml",
                        request.getLoginBehavior(), LoginActivity.class.getName()));
            }
        }
    }

    private void validatePermissions(AuthorizationRequest request, AuthorizationType authType) {
        if (request == null || Utility.isNullOrEmpty(request.getPermissions())) {
            if (AuthorizationType.PUBLISH.equals(authType)) {
                throw new FacebookException("Cannot request publish authorization with no permissions.");
            }
            return; // nothing to check
        }
        for (String permission : request.getPermissions()) {
            if (isPublishPermission(permission)) {
                if (AuthorizationType.READ.equals(authType)) {
                    throw new FacebookException(
                            String.format(
                                    "Cannot pass a publish permission (%s) to a request for read authorization",
                                    permission));
                }
            } else {
                if (AuthorizationType.PUBLISH.equals(authType)) {
                    Log.w(TAG,
                            String.format(
                                    "Should not pass a read permission (%s) to a request for publish authorization",
                                    permission));
                }
            }
        }
    }

    private boolean isPublishPermission(String permission) {
        return permission != null &&
                (permission.startsWith(PUBLISH_PERMISSION_PREFIX) ||
                        permission.startsWith(MANAGE_PERMISSION_PREFIX) ||
                        OTHER_PUBLISH_PERMISSIONS.contains(permission));

    }

    private boolean tryActivityAuth(Intent intent, AuthorizationRequest request, boolean validateSignature) {
        intent.putExtra("client_id", this.applicationId);

        if (!Utility.isNullOrEmpty(request.getPermissions())) {
            intent.putExtra("scope", TextUtils.join(",", request.getPermissions()));
        }

        if (!resolveIntent(intent, validateSignature)) {
            return false;
        }

        try {
            request.getStartActivityDelegate().startActivityForResult(intent, request.getRequestCode());
        } catch (ActivityNotFoundException e) {
            return false;
        }
        return true;
    }

    private boolean resolveIntent(Intent intent, boolean validateSignature) {
        ResolveInfo resolveInfo = getStaticContext().getPackageManager().resolveActivity(intent, 0);
        if ((resolveInfo == null) ||
                (validateSignature && !validateFacebookAppSignature(resolveInfo.activityInfo.packageName))) {
            return false;
        }
        return true;
    }

    private boolean tryDialogAuth(final AuthorizationRequest request) {
        Intent intent = new Intent();

        intent.setClass(getStaticContext(), LoginActivity.class);
        if (tryActivityAuth(intent, request, false)) {
            return true;
        }

        Log.w(TAG,
                String.format("Please add %s as an activity to your AndroidManifest.xml",
                        LoginActivity.class.getName()));

        int permissionCheck = getStaticContext().checkCallingOrSelfPermission(Manifest.permission.INTERNET);
        Activity activityContext = request.getStartActivityDelegate().getActivityContext();
        if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
            AlertDialog.Builder builder = new AlertDialog.Builder(activityContext);
            builder.setTitle("AndroidManifest Error");
            builder.setMessage("WebView login requires INTERNET permission");
            builder.create().show();
            return false;
        }

        Bundle parameters = new Bundle();
        if (!Utility.isNullOrEmpty(request.getPermissions())) {
            String scope = TextUtils.join(",", request.getPermissions());
            parameters.putString(ServerProtocol.DIALOG_PARAM_SCOPE, scope);
        }

        // The call to clear cookies will create the first instance of CookieSyncManager if necessary
        Utility.clearFacebookCookies(getStaticContext());

        DialogListener listener = new DialogListener() {
            public void onComplete(Bundle bundle) {
                // Ensure any cookies set by the dialog are saved
                CookieSyncManager.getInstance().sync();
                AccessToken newToken = AccessToken.createFromDialog(request.getPermissions(), bundle);
                Session.this.authorizationBundle = bundle;
                Session.this.finishAuth(newToken, null);
            }

            public void onError(DialogError error) {
                Bundle bundle = new Bundle();
                bundle.putInt(WEB_VIEW_ERROR_CODE_KEY, error.getErrorCode());
                bundle.putString(WEB_VIEW_FAILING_URL_KEY, error.getFailingUrl());
                Session.this.authorizationBundle = bundle;

                Exception exception = new FacebookAuthorizationException(error.getMessage());
                Session.this.finishAuth(null, exception);
            }

            public void onFacebookError(FacebookError error) {
                Exception exception = new FacebookAuthorizationException(error.getMessage());
                Session.this.finishAuth(null, exception);
            }

            public void onCancel() {
                Exception exception = new FacebookOperationCanceledException("User canceled log in.");
                Session.this.finishAuth(null, exception);
            }
        };

        parameters.putString(ServerProtocol.DIALOG_PARAM_DISPLAY, "touch");
        parameters.putString(ServerProtocol.DIALOG_PARAM_REDIRECT_URI, "fbconnect://success");
        parameters.putString(ServerProtocol.DIALOG_PARAM_TYPE, "user_agent");
        parameters.putString(ServerProtocol.DIALOG_PARAM_CLIENT_ID, this.applicationId);

        Uri uri = Utility.buildUri(ServerProtocol.DIALOG_AUTHORITY, ServerProtocol.DIALOG_OAUTH_PATH, parameters);
        new FbDialog(activityContext, uri.toString(), listener).show();

        return true;
    }

    private boolean tryKatanaProxyAuth(AuthorizationRequest request) {
        Intent intent = new Intent();

        intent.setClassName(NativeProtocol.KATANA_PACKAGE, NativeProtocol.KATANA_PROXY_AUTH_ACTIVITY);
        return tryActivityAuth(intent, request, true);
    }

    private boolean validateFacebookAppSignature(String packageName) {
        PackageInfo packageInfo = null;
        try {
            packageInfo = staticContext.getPackageManager().getPackageInfo(packageName,
                    PackageManager.GET_SIGNATURES);
        } catch (NameNotFoundException e) {
            return false;
        }

        for (Signature signature : packageInfo.signatures) {
            if (signature.toCharsString().equals(NativeProtocol.KATANA_SIGNATURE)) {
                return true;
            }
        }

        return false;
    }

    @SuppressWarnings("incomplete-switch")
    void finishAuth(AccessToken newToken, Exception exception) {
        // If the token we came up with is expired/invalid, then auth failed.
        if ((newToken != null) && newToken.isInvalid()) {
            newToken = null;
            exception = new FacebookException("Invalid access token.");
        }

        // Update the cache if we have a new token.
        if ((newToken != null) && (this.tokenCache != null)) {
            this.tokenCache.save(newToken.toCacheBundle());
        }

        synchronized (this.lock) {
            final SessionState oldState = this.state;

            switch (this.state) {
            case OPENING:
            case OPENED:
            case OPENED_TOKEN_UPDATED:
                if (newToken != null) {
                    this.tokenInfo = newToken;
                    this.state = (oldState == SessionState.OPENING) ? SessionState.OPENED
                            : SessionState.OPENED_TOKEN_UPDATED;
                } else if (exception != null) {
                    this.state = (oldState == SessionState.OPENING) ? SessionState.CLOSED_LOGIN_FAILED
                            : oldState;
                }
                postStateChange(oldState, this.state, exception);
                break;
            }
            pendingRequest = null;
        }
    }

    void postStateChange(final SessionState oldState, final SessionState newState, final Exception exception) {
        synchronized(callbacks) {
            // Need to schedule the callbacks inside the same queue to preserve ordering.
            // Otherwise these callbacks could have been added to the queue before the SessionTracker
            // gets the ACTIVE_SESSION_SET action.
            Runnable runCallbacks = new Runnable() {
                public void run() {
                    for (final StatusCallback callback : callbacks) {
                        Runnable closure = new Runnable() {
                            public void run() {
                                // This can be called inside a synchronized block.
                                callback.call(Session.this, newState, exception);
                            }
                        };
        
                        runWithHandlerOrExecutor(handler, closure);
                    }
                }
            };
            runWithHandlerOrExecutor(handler, runCallbacks);
        }

        if (this == Session.activeSession) {
            if (oldState.isOpened() != newState.isOpened()) {
                if (newState.isOpened()) {
                    postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_OPENED);
                } else {
                    postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_CLOSED);
                }
            }
        }
    }

    static void postActiveSessionAction(String action) {
        final Intent intent = new Intent(action);

        LocalBroadcastManager.getInstance(getStaticContext()).sendBroadcast(intent);
    }

    private static void runWithHandlerOrExecutor(Handler handler, Runnable runnable) {
        if (handler != null) {
            handler.post(runnable);
        } else {
            Settings.getExecutor().execute(runnable);
        }
    }

    void extendAccessTokenIfNeeded() {
        if (shouldExtendAccessToken()) {
            extendAccessToken();
        }
    }

    void extendAccessToken() {
        TokenRefreshRequest newTokenRefreshRequest = null;
        synchronized (this.lock) {
            if (currentTokenRefreshRequest == null) {
                newTokenRefreshRequest = new TokenRefreshRequest();
                currentTokenRefreshRequest = newTokenRefreshRequest;
            }
        }

        if (newTokenRefreshRequest != null) {
            newTokenRefreshRequest.bind();
        }
    }

    boolean shouldExtendAccessToken() {
        if (currentTokenRefreshRequest != null) {
            return false;
        }

        boolean result = false;

        Date now = new Date();

        if (state.isOpened() && tokenInfo.getIsSSO()
                && now.getTime() - lastAttemptedTokenExtendDate.getTime() > TOKEN_EXTEND_RETRY_SECONDS * 1000
                && now.getTime() - tokenInfo.getLastRefresh().getTime() > TOKEN_EXTEND_THRESHOLD_SECONDS * 1000) {
            result = true;
        }

        return result;
    }

    AccessToken getTokenInfo() {
        return tokenInfo;
    }

    void setTokenInfo(AccessToken tokenInfo) {
        this.tokenInfo = tokenInfo;
    }

    Date getLastAttemptedTokenExtendDate() {
        return lastAttemptedTokenExtendDate;
    }

    void setLastAttemptedTokenExtendDate(Date lastAttemptedTokenExtendDate) {
        this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate;
    }

    void setCurrentTokenRefreshRequest(TokenRefreshRequest request) {
        this.currentTokenRefreshRequest = request;
    }

    static String getMetadataApplicationId(Context context) {
        try {
            ApplicationInfo ai = context.getPackageManager().getApplicationInfo(
                    context.getPackageName(), PackageManager.GET_META_DATA);
            if (ai.metaData != null) {
                return ai.metaData.getString(APPLICATION_ID_PROPERTY);
            }
        } catch (NameNotFoundException e) {
            // if we can't find it in the manifest, just return null
        }

        return null;
    }

    class TokenRefreshRequest implements ServiceConnection {

        final Messenger messageReceiver = new Messenger(
                new TokenRefreshRequestHandler(Session.this, this));

        Messenger messageSender = null;

        public void bind() {
            Intent intent = new Intent();
            intent.setClassName(NativeProtocol.KATANA_PACKAGE, NativeProtocol.KATANA_TOKEN_REFRESH_ACTIVITY);

            ResolveInfo resolveInfo = staticContext.getPackageManager().resolveService(intent, 0);
            if (resolveInfo != null && validateFacebookAppSignature(resolveInfo.serviceInfo.packageName)
                    && staticContext.bindService(intent, new TokenRefreshRequest(), Context.BIND_AUTO_CREATE)) {
                setLastAttemptedTokenExtendDate(new Date());
            } else {
                cleanup();
            }
        }

        @Override
        public void onServiceConnected(ComponentName className, IBinder service) {
            messageSender = new Messenger(service);
            refreshToken();
        }

        @Override
        public void onServiceDisconnected(ComponentName arg) {
            cleanup();

            // We returned an error so there's no point in
            // keeping the binding open.
            staticContext.unbindService(TokenRefreshRequest.this);
        }

        private void cleanup() {
            if (currentTokenRefreshRequest == this) {
                currentTokenRefreshRequest = null;
            }
        }

        private void refreshToken() {
            Bundle requestData = new Bundle();
            requestData.putString(AccessToken.ACCESS_TOKEN_KEY, getTokenInfo().getToken());

            Message request = Message.obtain();
            request.setData(requestData);
            request.replyTo = messageReceiver;

            try {
                messageSender.send(request);
            } catch (RemoteException e) {
                cleanup();
            }
        }
        
    }

    // Creating a static Handler class to reduce the possibility of a memory leak.
    // Handler objects for the same thread all share a common Looper object, which they post messages
    // to and read from. As messages contain target Handler, as long as there are messages with target
    // handler in the message queue, the handler cannot be garbage collected. If handler is not static,
    // the instance of the containing class also cannot be garbage collected even if it is destroyed.
    static class TokenRefreshRequestHandler extends Handler {

        private WeakReference<Session> sessionWeakReference;
        private WeakReference<TokenRefreshRequest> refreshRequestWeakReference;

        TokenRefreshRequestHandler(Session session, TokenRefreshRequest refreshRequest) {
            super(Looper.getMainLooper());
            sessionWeakReference = new WeakReference<Session>(session);
            refreshRequestWeakReference = new WeakReference<TokenRefreshRequest>(refreshRequest);
        }

        @Override
        public void handleMessage(Message msg) {
            String token = msg.getData().getString(AccessToken.ACCESS_TOKEN_KEY);
            Session session = sessionWeakReference.get();

            if (session != null && token != null) {
                session.internalRefreshToken(msg.getData());
            }

            TokenRefreshRequest request = refreshRequestWeakReference.get();
            if (request != null) {
                // The refreshToken function should be called rarely,
                // so there is no point in keeping the binding open.
                staticContext.unbindService(request);
                request.cleanup();
            }
        }
    }

    /**
     * Provides asynchronous notification of Session state changes.
     * 
     * @see Session#open open
     */
    public interface StatusCallback {
        public void call(Session session, SessionState state, Exception exception);
    }
    
    @Override
    public int hashCode() {
        return 0;
    }
    
    @Override
    public boolean equals(Object otherObj) {
        if (!(otherObj instanceof Session)) {
            return false;
        }
        Session other = (Session) otherObj;
        
        return areEqual(other.applicationId, applicationId) &&
                areEqual(other.authorizationBundle, authorizationBundle) &&
                areEqual(other.state, state) &&
                areEqual(other.getExpirationDate(), getExpirationDate());
    }
    
    private static boolean areEqual(Object a, Object b) {
        if (a == null) {
            return b == null;
        } else {
            return a.equals(b);
        }
    }

    /**
     * Builder class used to create a Session.
     */
    public static final class Builder {
        private final Context context;
        private String applicationId;
        private TokenCache tokenCache;
        private boolean shouldAutoPublishInstall = true;

        /**
         * Constructs a new Builder associated with the context.
         *
         * @param context the Activity or Service starting the Session
         */
        public Builder(Context context) {
            this.context = context;
        }

        /**
         * Sets the application id for the Session.
         *
         * @param applicationId the application id
         * @return the Builder instance
         */
        public Builder setApplicationId(final String applicationId) {
            this.applicationId = applicationId;
            return this;
        }

        /**
         * Sets the TokenCache for the Session.
         *
         * @param tokenCache the token cache to use
         * @return the Builder instance
         */
        public Builder setTokenCache(final TokenCache tokenCache) {
            this.tokenCache = tokenCache;
            return this;
        }

        public Builder setShouldAutoPublishInstall(boolean shouldAutoPublishInstall) {
            this.shouldAutoPublishInstall = shouldAutoPublishInstall;
            return this;
        }

        /**
         * Build the Session.
         *
         * @return a new Session
         */
        public Session build() {
            return new Session(context, applicationId, tokenCache, shouldAutoPublishInstall);
        }
    }

    private interface StartActivityDelegate {
        public void startActivityForResult(Intent intent, int requestCode);

        public Activity getActivityContext();
    }

    enum AuthorizationType {
        READ,
        PUBLISH
    }

    private void autoPublishAsync() {
        AutoPublishAsyncTask asyncTask = null;
        synchronized (this) {
            if (autoPublishAsyncTask == null && shouldAutoPublish) {
                // copy the application id to guarantee thread safety against our container.
                String applicationId = Session.this.applicationId;

                // skip publish if we don't have an application id.
                if (applicationId != null) {
                    asyncTask = autoPublishAsyncTask = new AutoPublishAsyncTask(applicationId, staticContext);
                }
            }
        }

        if (asyncTask != null) {
            asyncTask.execute();
        }
    }

    /**
     * Async implementation to allow auto publishing to not block the ui thread.
     */
    private class AutoPublishAsyncTask extends AsyncTask<Void, Void, Void> {
        private final String mApplicationId;
        private final Context mApplicationContext;

        public AutoPublishAsyncTask(String applicationId, Context context) {
            mApplicationId = applicationId;
            mApplicationContext = context.getApplicationContext();
        }

        @Override
        protected Void doInBackground(Void... voids) {
            try {
                Settings.publishInstallAndWait(mApplicationContext, mApplicationId);
            } catch (Exception e) {
                Util.logd("Facebook-publish", e.getMessage());
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            // always clear out the publisher to allow other invocations.
            synchronized (Session.this) {
                autoPublishAsyncTask = null;
            }
        }
    }

    public static class AuthorizationRequest implements Serializable {

        private static final long serialVersionUID = 1L;

        private final StartActivityDelegate startActivityDelegate;
        private SessionLoginBehavior loginBehavior = SessionLoginBehavior.SSO_WITH_FALLBACK;
        private int requestCode = DEFAULT_AUTHORIZE_ACTIVITY_CODE;
        private StatusCallback statusCallback;
        private boolean suppressLoginActivityVerification = false;
        private List<String> permissions = Collections.emptyList();

        AuthorizationRequest(final Activity activity) {
            startActivityDelegate = new StartActivityDelegate() {
                @Override
                public void startActivityForResult(Intent intent, int requestCode) {
                    activity.startActivityForResult(intent, requestCode);
                }

                @Override
                public Activity getActivityContext() {
                    return activity;
                }
            };
        }

        AuthorizationRequest(final Fragment fragment) {
            startActivityDelegate = new StartActivityDelegate() {
                @Override
                public void startActivityForResult(Intent intent, int requestCode) {
                    fragment.startActivityForResult(intent, requestCode);
                }

                @Override
                public Activity getActivityContext() {
                    return fragment.getActivity();
                }
            };
        }

        /**
         * Constructor to be used for V1 serialization only, DO NOT CHANGE.
         */
        private AuthorizationRequest(SessionLoginBehavior loginBehavior, int requestCode,
                List<String> permissions, boolean suppressLoginActivityVerification) {
            startActivityDelegate = new StartActivityDelegate() {
                @Override
                public void startActivityForResult(Intent intent, int requestCode) {
                    throw new UnsupportedOperationException(
                            "Cannot create an AuthorizationRequest without a valid Activity or Fragment");
                }

                @Override
                public Activity getActivityContext() {
                    throw new UnsupportedOperationException(
                            "Cannot create an AuthorizationRequest without a valid Activity or Fragment");
                }
            };
            this.loginBehavior = loginBehavior;
            this.requestCode = requestCode;
            this.permissions = permissions;
            this.suppressLoginActivityVerification = suppressLoginActivityVerification;
        }

        /**
         * Used for backwards compatibility with Facebook.java only, DO NOT USE.
         *
         * @param suppressVerification
         */
        public void suppressLoginActivityVerification(boolean suppressVerification) {
            suppressLoginActivityVerification = suppressVerification;
        }

        AuthorizationRequest setCallback(StatusCallback statusCallback) {
            this.statusCallback = statusCallback;
            return this;
        }

        StatusCallback getCallback() {
            return statusCallback;
        }

        AuthorizationRequest setLoginBehavior(SessionLoginBehavior loginBehavior) {
            if (loginBehavior != null) {
                this.loginBehavior = loginBehavior;
            }
            return this;
        }

        SessionLoginBehavior getLoginBehavior() {
            return loginBehavior;
        }

        AuthorizationRequest setRequestCode(int requestCode) {
            if (requestCode >= 0) {
                this.requestCode = requestCode;
            }
            return this;
        }

        int getRequestCode() {
            return requestCode;
        }

        AuthorizationRequest setPermissions(List<String> permissions) {
            if (permissions != null) {
                this.permissions = permissions;
            }
            return this;
        }

        List<String> getPermissions() {
            return permissions;
        }

        StartActivityDelegate getStartActivityDelegate() {
            return startActivityDelegate;
        }

        boolean allowKatana() {
            switch (loginBehavior) {
                case SSO_ONLY: return true;
                case SUPPRESS_SSO: return false;
                default: return true;
            }
        }

        boolean allowWebView() {
            switch (loginBehavior) {
                case SSO_ONLY: return false;
                case SUPPRESS_SSO: return true;
                default: return true;
            }
        }

        // package private so subclasses can use it
        Object writeReplace() {
            return new AuthRequestSerializationProxyV1(loginBehavior, requestCode, permissions,
                    suppressLoginActivityVerification);
        }

        // have a readObject that throws to prevent spoofing
        // package private so subclasses can use it
        void readObject(ObjectInputStream stream) throws InvalidObjectException {
            throw new InvalidObjectException("Cannot readObject, serialization proxy required");
        }

        private static class AuthRequestSerializationProxyV1 implements Serializable {
            private static final long serialVersionUID = -8748347685113614927L;
            private final SessionLoginBehavior loginBehavior;
            private final int requestCode;
            private boolean suppressLoginActivityVerification;
            private final List<String> permissions;

            private AuthRequestSerializationProxyV1(SessionLoginBehavior loginBehavior,
                    int requestCode, List<String> permissions, boolean suppressVerification) {
                this.loginBehavior = loginBehavior;
                this.requestCode = requestCode;
                this.permissions = permissions;
                this.suppressLoginActivityVerification = suppressVerification;
            }

            private Object readResolve() {
                return new AuthorizationRequest(loginBehavior, requestCode, permissions,
                        suppressLoginActivityVerification);
            }
        }
    }

    /**
     * A request used to open a Session.
     */
    public static final class OpenRequest extends AuthorizationRequest {
        private static final long serialVersionUID = 1L;

        /**
         * Constructs an OpenRequest.
         *
         * @param activity the Activity to use to open the Session
         */
        public OpenRequest(Activity activity) {
            super(activity);
        }

        /**
         * Constructs an OpenRequest.
         *
         * @param fragment the Fragment to use to open the Session
         */
        public OpenRequest(Fragment fragment) {
            super(fragment);
        }

        /**
         * Sets the StatusCallback for the OpenRequest.
         *
         * @param statusCallback
         *            The {@link StatusCallback SessionStatusCallback} to
         *            notify regarding Session state changes.
         * @return the OpenRequest object to allow for chaining
         */
        public final OpenRequest setCallback(StatusCallback statusCallback) {
            super.setCallback(statusCallback);
            return this;
        }

        /**
         * Sets the login behavior for the OpenRequest.
         *
         * @param loginBehavior
         *            The {@link SessionLoginBehavior SessionLoginBehavior} that
         *            specifies what behaviors should be attempted during
         *            authorization.
         * @return the OpenRequest object to allow for chaining
         */
        public final OpenRequest setLoginBehavior(SessionLoginBehavior loginBehavior) {
            super.setLoginBehavior(loginBehavior);
            return this;
        }

        /**
         * Sets the request code for the OpenRequest.
         *
         * @param requestCode
         *            An integer that identifies this request. This integer will be used
         *            as the request code in {@link Activity#onActivityResult
         *            onActivityResult}. This integer should be >= 0. If a value < 0 is
         *            passed in, then a default value will be used.
         * @return the OpenRequest object to allow for chaining
         */
        public final OpenRequest setRequestCode(int requestCode) {
            super.setRequestCode(requestCode);
            return this;
        }

        /**
         * Sets the permissions for the OpenRequest.
         *
         * @param permissions
         *            A List&lt;String&gt; representing the permissions to request
         *            during the authentication flow. A null or empty List
         *            represents basic permissions.
         * @return the OpenRequest object to allow for chaining
         */
        public final OpenRequest setPermissions(List<String> permissions) {
            super.setPermissions(permissions);
            return this;
        }
    }

    /**
     * A request to be used to reauthorize a Session.
     */
    public static final class ReauthorizeRequest extends AuthorizationRequest {
        private static final long serialVersionUID = 1L;

        /**
         * Constructs a ReauthorizeRequest.
         *
         * @param activity the Activity used to reauthorize
         * @param permissions additional permissions to request
         */
        public ReauthorizeRequest(Activity activity, List<String> permissions) {
            super(activity);
            setPermissions(permissions);
        }

        /**
         * Constructs a ReauthorizeRequest.
         *
         * @param fragment the Fragment used to reauthorize
         * @param permissions additional permissions to request
         */
        public ReauthorizeRequest(Fragment fragment, List<String> permissions) {
            super(fragment);
            setPermissions(permissions);
        }

        /**
         * Sets the StatusCallback for the ReauthorizeRequest.
         *
         * @param statusCallback
         *            The {@link StatusCallback SessionStatusCallback} to
         *            notify regarding Session state changes.
         * @return the ReauthorizeRequest object to allow for chaining
         */
        public final ReauthorizeRequest setCallback(StatusCallback statusCallback) {
            super.setCallback(statusCallback);
            return this;
        }

        /**
         * Sets the login behavior for the ReauthorizeRequest.
         *
         * @param loginBehavior
         *            The {@link SessionLoginBehavior SessionLoginBehavior} that
         *            specifies what behaviors should be attempted during
         *            authorization.
         * @return the ReauthorizeRequest object to allow for chaining
         */
        public final ReauthorizeRequest setLoginBehavior(SessionLoginBehavior loginBehavior) {
            super.setLoginBehavior(loginBehavior);
            return this;
        }

        /**
         * Sets the request code for the ReauthorizeRequest.
         *
         * @param requestCode
         *            An integer that identifies this request. This integer will be used
         *            as the request code in {@link Activity#onActivityResult
         *            onActivityResult}. This integer should be >= 0. If a value < 0 is
         *            passed in, then a default value will be used.
         * @return the ReauthorizeRequest object to allow for chaining
         */
        public final ReauthorizeRequest setRequestCode(int requestCode) {
            super.setRequestCode(requestCode);
            return this;
        }
    }
}
